home *** CD-ROM | disk | FTP | other *** search
/ Delphi Magazine Collection 2001 / Delphi Magazine Collection 20001 (2001).iso / DISKS / Issue68 / sync / GpFileSync.pas < prev    next >
Encoding:
Pascal/Delphi Source File  |  2001-02-28  |  33.4 KB  |  1,009 lines

  1. {:File system-based synchronisation primitives.
  2.   (c) 1999-2001 Primoz Gabrijelcic
  3.  
  4.   @author Primoz Gabrijelcic
  5.   @desc <pre>
  6.  
  7.   Free for personal and commercial use.
  8.   Tested with Delphi 5. Should work with Delphi 4 but not with older versions.
  9.  
  10.   Author           : Primoz Gabrijelcic
  11.   Creation date    : 1999-12-02
  12.   Last modification: 2001-02-28
  13.   Version          : 1.06
  14.  
  15.   </pre>}{
  16.  
  17.   History:
  18.   1.06: 2001-02-28
  19.     - Added TGpFileSWMR class.
  20.  
  21.   1.05: 2001-02-26
  22.     - Semantics for TGpCriticalSection.Enter/Leave. It is now possible to
  23.       recursively nest .Enter/.Leave calls. (For example - this sequence is now
  24.       valid:
  25.         cs.Enter;
  26.         cs.Enter;
  27.         cs.Leave; //CS still owned
  28.         cs.Leave; //CS now unowned
  29.       This makes TGpCriticalSection behave more like standard critical section
  30.       in Windows.
  31.  
  32.   1.04: 2001-01-06
  33.     - All classes renamed from T* to TGp*, exception EFileSync renamed to
  34.       EGpFileSync.
  35.  
  36.   1.03: 2000-11-16
  37.     - Added 'timeout' parameter to TFileGroup.Join.
  38.     - Changed TFileMutex.Acquire, TFileMutex.Release, TFileGroup.Join,
  39.       TFileGroup.Leave to raise EFileSync exception on various programmer's
  40.       errors.
  41.     - Added TFileGroup.IsMember.
  42.  
  43.   1.01: 2000-11-12
  44.     - Property TFileSynchroObject.RetryDelay made read/write and public
  45.       (was readonly and protected).
  46.     - TFileMutex.Acquire modified to raise exception if mutex was already
  47.       acquired (was returning 'true'). This also applies to
  48.       TFileCriticalSection.
  49.  
  50.   1.0: 2000-07-06
  51.     - New class TFileMessage - enhancement of TFileEvent, capable of
  52.       transmitting data.
  53.     - Flowchart: GpFileSync.vsd.
  54.     - TFileMutex.Acquired and TFileMutex.Release were testing handle against 0
  55.       instead of INVALID_HANDLE_VALUE. Fixed.
  56.  
  57.   0.1: 1999-12-19
  58.     - Fixed External exception on Leave.
  59.  
  60.   0.0: 1999-12-02
  61.     - First alpha version.
  62. }
  63.  
  64. unit GpFileSync;
  65.  
  66. interface
  67.  
  68. uses
  69.   Windows,
  70.   SysUtils;
  71.  
  72. const
  73.   //:Default delay between retries in milliseconds.
  74.   CDefRetryDelay = 100;
  75.  
  76. type
  77.   {:Ancestor of all file synchronisation objects.}
  78.   TGpFileSynchroObject = class
  79.   private
  80.     fsoFileName  : string;
  81.     fsoRetryDelay: integer;
  82.   protected
  83.     constructor Create(syncFile: string; alwaysCheckForWriteAcc: boolean);
  84.     procedure CheckForWriteAccess(folder: string);
  85.     procedure SetRetryDelay(const Value: integer); virtual;
  86.   public
  87.     property  RetryDelay: integer read fsoRetryDelay write SetRetryDelay;
  88.     property  SyncFile: string read fsoFileName;
  89.   end; { TGpFileSynchroObject }
  90.  
  91.   {:File system-based mutex. Needs write access to lock folder or existing lock
  92.     file. If process crashes while holding mutex, it will be automatically
  93.     released.
  94.   }
  95.   TGpFileMutex = class(TGpFileSynchroObject)
  96.   private
  97.     fmDelete: boolean;
  98.     fmHandle: THandle;
  99.   public
  100.     constructor Create(syncFile: string; deleteOnRelease: boolean = false); reintroduce;
  101.     destructor  Destroy; override;
  102.     function  Acquire(timeout: DWORD): boolean;
  103.     function  Acquired: boolean;
  104.     function  IsFree(timeout: DWORD): boolean;
  105.     procedure Release;
  106.   end; { TGpFileMutex }
  107.  
  108.   {:File system-based critical section. Just a mutex with simplified access.}
  109.   TGpFileCriticalSection = class(TGpFileMutex)
  110.   private
  111.     nestCount: integer;
  112.   public
  113.     procedure Acquire; reintroduce;
  114.     procedure Enter;
  115.     procedure Leave;
  116.     procedure Release; reintroduce;
  117.   end; { TGpFileCriticalSection }
  118.  
  119.   {:File system-based group. Needs write access to sync folder or existing
  120.     sync file. Member counting is not implemented. If process crashes, it will
  121.     automatically leave the group. In addition to syncFile, TFileGroup uses
  122.     additional lock file with extension '_lck' and same full name. If, for
  123.     example, sync file is named 'myapp.grp', group lock file will be named
  124.     'myapp.grp_lck'.
  125.   }
  126.   TGpFileGroup = class(TGpFileSynchroObject)
  127.   private
  128.     fgDelete: boolean;
  129.     fgHandle: THandle;
  130.     fgLock  : TGpFileMutex;
  131.   protected
  132.     procedure SetRetryDelay(const Value: integer); override;
  133.   public
  134.     constructor Create(syncFile: string; deleteOnRelease: boolean = false); reintroduce;
  135.     destructor  Destroy; override;
  136.     function  IsEmpty(timeout: DWORD; var emptyGroup: boolean): boolean;
  137.     function  IsMember: boolean;
  138.     function  Join(timeout: DWORD; var isFirstMember: boolean): boolean; overload;
  139.     function  Join(timeout: DWORD): boolean; overload;
  140.     function  Leave(timeout: DWORD; var wasLastMember: boolean): boolean; overload;
  141.     function  Leave(timeout: DWORD): boolean; overload;
  142.   end; { TGpFileGroup }
  143.  
  144.   {:File system-based Single Writer Multiple Readers (SWMR) synchronisation
  145.     primitive. Uses two mutexes (with extension '_lck1' and '_lck2') and one
  146.     group (using two files - one with extension '_grp' and other with
  147.     '_grp_lck'. Synchronisation file by itself is never used.
  148.     @since   2001-02-28 (1.06)
  149.   }
  150.   TGpFileSWMR = class
  151.   private
  152.     fswmrGroup       : TGpFileGroup;
  153.     fswmrMutex1      : TGpFileMutex;
  154.     fswmrMutex2      : TGpFileMutex;
  155.     fswmrSyncFileBase: string;
  156.     fswmrRetryDelay: integer;
  157.     procedure SetRetryDelay(const Value: integer);
  158.   public
  159.     constructor Create(syncFileBase: string; deleteOnRelease: boolean = false); reintroduce;
  160.     destructor  Destroy; override;
  161.     function  DoneReading(timeout: DWORD): boolean;
  162.     procedure DoneWriting;
  163.     function  IsReading: boolean;
  164.     function  IsWriting: boolean;
  165.     function  WaitToRead(timeout: DWORD): boolean;
  166.     function  WaitToWrite(timeout: DWORD): boolean;
  167.     property  RetryDelay: integer read fswmrRetryDelay write SetRetryDelay;
  168.     property  SyncFile: string read fswmrSyncFileBase;
  169.   end; { TGpFileSWMR }
  170.  
  171.   {:File system-based event. Needs write access to folder with sync file.
  172.     Persistent by design (synchronisation file is not deleted if signalling
  173.     application crashes).
  174.   }
  175.   TGpFileEvent = class(TGpFileSynchroObject)
  176.   public
  177.     constructor Create(syncFile: string); reintroduce;
  178.     function Reset: boolean;
  179.     function Signal: boolean;
  180.     function WaitFor(timeout: DWORD; reset: boolean): boolean;
  181.   end; { TGpFileEvent }
  182.  
  183.   {:File system-based messaging. Needs write access to folder with sync file.
  184.     In addition to syncFile, TFileMessage uses lock file with extension '_lck'
  185.     and group files with extensions '_grp' and '_grp_lck', all using syncFile as
  186.     prefix. If, for example, message file is named 'myapp.msg', message lock
  187.     file will be named 'myapp.msg_lck' and message group files will be named
  188.     'myapp.msg_grp' and 'myapp.msg_grp_lck'.
  189.     TFileMessage is non-persistent by design.
  190.     TFileMessage assumes that there is only one sender and one receiver.
  191.     @since 2000-07-06 (1.0)
  192.   }
  193.   TGpFileMessage = class(TGpFileSynchroObject)
  194.   private
  195.     fmLock : TGpFileMutex;
  196.     fmGroup: TGpFileGroup;
  197.   protected
  198.     procedure SetRetryDelay(const Value: integer); override;
  199.   public
  200.     constructor Create(syncFile: string); reintroduce;
  201.     destructor  Destroy; override;
  202.     function  Receive(timeout: DWORD; var msg: pointer; var msgSize: integer): boolean;
  203.     function  Send(timeout: DWORD; msg: pointer; msgSize: integer): boolean;
  204.   end; { TGpFileMessage }
  205.  
  206.   EGpFileSync = class(Exception);
  207.  
  208. implementation
  209.  
  210. const
  211.   FILE_SHARING_ERRORS: set of byte =
  212.     [ERROR_SHARING_VIOLATION, ERROR_LOCK_VIOLATION];
  213.  
  214.   CAutoDestroyTimeout = 10000; // 5 seconds
  215.  
  216. resourcestring
  217.   SAlreadyAcquired     = 'Already acquired: %s.';
  218.   SAlreadyJoined       = 'Already joined: %s.';
  219.   SAlreadyReading      = 'Already reading: %s.';
  220.   SAlreadyWriting      = 'Already writing: %s.';
  221.   SCannotAccessFile    = 'Cannot access file %s. Error: "%s"';
  222.   SCannotAcquireMutex  = 'Cannot acquire mutex %s.';
  223.   SCannotCreateFile    = 'Cannot create file %s. Error: "%s"';
  224.   SCannotDeleteFile    = 'Cannot delete file %s. Error: "%s"';
  225.   SCannotJoinGroup     = 'Cannot join group %s.';
  226.   SCannotMoveFilePtr   = 'Cannot move file pointer for file %s to offset %d. Error: "%s"';
  227.   SCannotReadFromFile  = 'Cannot read from file %s. Error: "%s"';
  228.   SCannotWriteToFile   = 'Cannot write to file %s. Error: "%s"';
  229.   SGroupNotEmpty       = 'Group %s is not empty.';
  230.   SIncorrectNumRead    = 'Incorrect number of bytes read from file %s.';
  231.   SIncorrectNumWritten = 'Incorrect number of bytes written to file %s.';
  232.   SNotAcquired         = 'Not acquired: %s.';
  233.   SNotJoined           = 'Not member: %s.';
  234.   SNotReading          = 'Not reading: %s.';
  235.   SNotWriting          = 'Not writing: %s.';
  236.   SNoWriteAccess       = 'Cannot write to folder %s.';
  237.  
  238. {:Checks if more than 'timeout' time has elapsed since 'start'. Supports
  239.   INFINITE.
  240. }
  241. function Elapsed(start: int64; timeout: DWORD): boolean;
  242. var
  243.   stop: int64;
  244. begin
  245.   if timeout = 0 then
  246.     Result := true
  247.   else if timeout = INFINITE then
  248.     Result := false
  249.   else begin
  250.     stop := GetTickCount;
  251.     if stop < start then
  252.       stop := stop + $100000000;
  253.     Result := ((stop-start) > timeout);
  254.   end;
  255. end; { Elapsed }
  256.  
  257. { TGpFileSynchroObject }
  258.  
  259. {:Checks if process has write access to folder.
  260.   @param   folder Folder being checked.
  261.   @raises  EGpFileSync if write access is not allowed.
  262. }
  263. procedure TGpFileSynchroObject.CheckForWriteAccess(folder: string);
  264. var
  265.   uid: UINT;
  266.   buf: array [0..MAX_PATH] of char;
  267. begin
  268.   uid := GetTempFileName(PChar(folder),'fso',0,buf);
  269.   if uid = 0 then
  270.     raise EGpFileSync.CreateFmt(SNoWriteAccess,[folder])
  271.   else
  272.     DeleteFile(buf);
  273. end; { TGpFileSynchroObject.CheckForWriteAccess }
  274.  
  275. {:Base constructor. Optionally checks for write access to synchronisation folder.
  276.   @raises  EGpFileSync 
  277. }
  278. constructor TGpFileSynchroObject.Create(syncFile: string;
  279.   alwaysCheckForWriteAcc: boolean);
  280. begin
  281.   fsoRetryDelay := CDefRetryDelay; // ms
  282.   fsoFileName := ExpandFileName(syncFile);
  283.   UniqueString(fsoFileName);
  284.   if alwaysCheckForWriteAcc or (not FileExists(fsoFileName)) then
  285.     CheckForWriteAccess(ExtractFilePath(fsoFileName));
  286. end; { TGpFileSynchroObject.Create }
  287.  
  288. {:Set RetryDelay property.
  289. }
  290. procedure TGpFileSynchroObject.SetRetryDelay(const Value: integer);
  291. begin
  292.   fsoRetryDelay := Value;
  293. end; { TGpFileSynchroObject.SetRetryDelay }
  294.  
  295. { TGpFileMutex }
  296.  
  297. {:Tries to acquire mutex.
  298.   @param   timeout Timeout in milliseconds. 0 and INFINITE are supported.
  299.   @returns true if mutex was acquired.
  300.   @raises  EGpFileSync if sync file cannot be created of if mutex is already
  301.            acquired.
  302. }
  303. function TGpFileMutex.Acquire(timeout: DWORD): boolean;
  304. var
  305.   flag : DWORD;
  306.   err  : DWORD;
  307.   start: int64;
  308. begin
  309.   if Acquired then
  310.     raise EGpFileSync.CreateFmt(SAlreadyAcquired,[SyncFile])
  311.   else begin
  312.     flag := FILE_ATTRIBUTE_NORMAL;
  313.     if fmDelete then
  314.       flag := flag OR FILE_FLAG_DELETE_ON_CLOSE;
  315.     start := GetTickCount;
  316.     repeat
  317.       fmHandle := CreateFile(PChar(SyncFile),GENERIC_READ,0,nil,OPEN_ALWAYS,flag,0);
  318.       if fmHandle = INVALID_HANDLE_VALUE then begin
  319.         err := GetLastError;
  320.         if err in FILE_SHARING_ERRORS then
  321.           Sleep(RetryDelay)
  322.         else
  323.           raise EGpFileSync.CreateFmt(SCannotAccessFile,[SyncFile,SysErrorMessage(err)]);
  324.       end
  325.       else
  326.         err := 0;
  327.     until (err = 0) or Elapsed(start,timeout);
  328.     Result := (err = 0);
  329.   end;
  330. end; { TGpFileMutex.Acquire }
  331.  
  332. {:Checks if mutex is acquired.
  333.   returns true if mutex is currently acquired.
  334. }
  335. function TGpFileMutex.Acquired: boolean;
  336. begin
  337.   Result := (fmHandle <> INVALID_HANDLE_VALUE);
  338. end; { TGpFileMutex.Acquired }
  339.  
  340. {:TGpFileMutex constructor. 
  341.   @param deleteOnRelease If set, synchronisation file will be deleted on Release.
  342.                          This requires write access to synchronisation folder.
  343.   @raises EGpFileSync if mutex file does not exist and write access to mutex file
  344.           folder is not allowed.
  345. }
  346. constructor TGpFileMutex.Create(syncFile: string; deleteOnRelease: boolean);
  347. begin
  348.   inherited Create(syncFile,false);
  349.   fmDelete := deleteOnRelease;
  350.   fmHandle := INVALID_HANDLE_VALUE;
  351. end; { TGpFileMutex.Create }
  352.  
  353. {:TGpFileMutex destructor. Releases mutex if acquired.
  354. }
  355. destructor TGpFileMutex.Destroy;
  356. begin
  357.   if Acquired then
  358.     Release;
  359. end; { TGpFileMutex.Destroy }
  360.  
  361. {:Checks if mutex can be acquired but does not acquire it.
  362.   @returns true if mutex can be acquired.
  363.   @raises  EGpFileSync if sync file cannot be created.
  364. }
  365. function TGpFileMutex.IsFree(timeout: DWORD): boolean;
  366. begin
  367.   if Acquired then
  368.     Result := false
  369.   else begin
  370.     if Acquire(timeout) then begin
  371.       Result := true;
  372.       Release;
  373.     end
  374.     else
  375.       Result := false;
  376.   end;
  377. end; { TGpFileMutex.IsFree }
  378.  
  379. {:Releases mutex.
  380.   @raises EGpFileSync if mutex is not acquired.
  381. }
  382. procedure TGpFileMutex.Release;
  383. begin
  384.   if Acquired then begin
  385.     CloseHandle(fmHandle);
  386.     fmHandle := INVALID_HANDLE_VALUE;
  387.   end
  388.   else
  389.     raise EGpFileSync.CreateFmt(SNotAcquired,[SyncFile]);
  390. end; { TGpFileMutex.Release }
  391.  
  392. { TGpFileCriticalSection }
  393.  
  394. {:Acquires critical section if not already acquired otherwise just increments
  395.   nesting count.
  396.   @raises EGpFileSync if sync file cannot be created.
  397. }
  398. procedure TGpFileCriticalSection.Acquire;
  399. begin
  400.   if nestCount = 0 then
  401.     inherited Acquire(INFINITE);
  402.   Inc(nestCount);
  403. end; { TGpFileCriticalSection.Acquire }
  404.  
  405. {:Synonim for Acquire.
  406. }
  407. procedure TGpFileCriticalSection.Enter;
  408. begin
  409.   Acquire;
  410. end; { TGpFileCriticalSection.Enter }
  411.  
  412. {:Synonym for Release.
  413. }
  414. procedure TGpFileCriticalSection.Leave;
  415. begin
  416.   Release;
  417. end; { TGpFileCriticalSection.Leave }
  418.  
  419. {:Decrements nesting count and releases critical section when it drops to zero.
  420.   @raises EGpFileSync if critical section is not owned.
  421. }
  422. procedure TGpFileCriticalSection.Release;
  423. begin
  424.   Dec(nestCount);
  425.   if nestCount <= 0 then
  426.     inherited Release;
  427. end; { TGpFileCriticalSection.Release }
  428.  
  429. { TGpFileEvent }
  430.  
  431. {:TGpFileEvent constructor.
  432.   @raises EGpFileSync if program doesn't have rights to create syncFile.
  433. }
  434. constructor TGpFileEvent.Create(syncFile: string);
  435. begin
  436.   inherited Create(syncFile,true);
  437. end; { TGpFileEvent.Create }
  438.  
  439. {:Resets event.
  440.   @returns false if event is not signaled.
  441.   @raises EGpFileSync if event file couldn't be deleted.
  442. }
  443. function TGpFileEvent.Reset: boolean;
  444. begin
  445.   if not FileExists(SyncFile) then
  446.     Reset := false
  447.   else begin
  448.     if Windows.DeleteFile(PChar(SyncFile)) then
  449.       Result := true
  450.     else
  451.       raise EGpFileSync.CreateFmt(SCannotDeleteFile,[SyncFile,SysErrorMessage(GetLastError)]);
  452.   end;
  453. end; { TGpFileEvent.Reset }
  454.  
  455. {:Signals event.
  456.   @returns false if event is already signaled.
  457.   @raises EGpFileSync if event file couldn't be created.
  458. }
  459. function TGpFileEvent.Signal: boolean;
  460. var
  461.   h  : THandle;
  462.   err: DWORD;
  463. begin
  464.   h := CreateFile(PChar(SyncFile),GENERIC_READ,0,nil,CREATE_NEW,FILE_ATTRIBUTE_NORMAL,0);
  465.   if h = INVALID_HANDLE_VALUE then begin
  466.     err := GetLastError;
  467.     if err = ERROR_FILE_EXISTS then
  468.       Result := false
  469.     else
  470.       raise EGpFileSync.CreateFmt(SCannotCreateFile,[SyncFile,SysErrorMessage(err)]);
  471.   end
  472.   else begin
  473.     CloseHandle(h);
  474.     Result := true;
  475.   end;
  476. end; { TGpFileEvent.Signal }
  477.  
  478. {:Waits for event and optionally resets it.
  479.   @param   timeout Timeout, 0 and INFINITE are supported.
  480.   @param   reset   If true, resets event.
  481.   @returns true if event was signaled before timeout.
  482.   @raises  EGpFileSync if sync file cannot be accessed.
  483. }
  484. function TGpFileEvent.WaitFor(timeout: DWORD; reset: boolean): boolean;
  485. var
  486.   flag : DWORD;
  487.   start: int64;
  488.   h    : THandle;
  489.   err  : DWORD;
  490. begin
  491.   flag := FILE_ATTRIBUTE_NORMAL;
  492.   if reset then
  493.     flag := flag OR FILE_FLAG_DELETE_ON_CLOSE;
  494.   start := GetTickCount;
  495.   repeat
  496.     h := CreateFile(PChar(SyncFile),GENERIC_READ,0,nil,OPEN_EXISTING,flag,0);
  497.     if h = INVALID_HANDLE_VALUE then begin
  498.       err := GetLastError;
  499.       if err in (FILE_SHARING_ERRORS+[ERROR_FILE_NOT_FOUND]) then
  500.         Sleep(RetryDelay)
  501.       else
  502.         raise EGpFileSync.CreateFmt(SCannotAccessFile,[SyncFile,SysErrorMessage(err)]);
  503.     end
  504.     else begin
  505.       CloseHandle(h);
  506.       err := 0;
  507.     end;
  508.   until (err = 0) or Elapsed(start,timeout);
  509.   Result := (err = 0);
  510. end; { TGpFileEvent.WaitFor }
  511.  
  512. { TGpFileGroup }
  513.  
  514. {:TGpFileGroup constructor.
  515.   @param  deleteOnRelease If set, synchronisation file will be deleted when last process
  516.           leaves it.
  517.   @raises EGpFileSync if syncFile does not exist and program doesn't have creation rights.
  518. }
  519. constructor TGpFileGroup.Create(syncFile: string; deleteOnRelease: boolean);
  520. begin
  521.   inherited Create(syncFile,false);
  522.   fgHandle := INVALID_HANDLE_VALUE;
  523.   fgDelete := deleteOnRelease;
  524.   fgLock   := TGpFileCriticalSection.Create(syncFile+'_lck',deleteOnRelease);
  525.   fgLock.RetryDelay := RetryDelay;
  526. end; { TGpFileGroup.Create }
  527.  
  528. {:TGpFileGroup destructor. Leaves group befor destroying it.
  529. }
  530. destructor TGpFileGroup.Destroy;
  531. begin
  532.   if IsMember then
  533.     Leave(CAutoDestroyTimeout);
  534.   fgLock.Free;
  535.   fgLock := nil;
  536. end; { TGpFileGroup.Destroy }
  537.  
  538. {:Joins the group.
  539.   @param   timeout       Timeout in milliseconds. 0 and INFINITE are supported.
  540.   @param   isFirstMember (out) Set to true if this was first member of the
  541.                          group. Defined only if function returns true.
  542.   @returns false on timeout.
  543.   @raises  EGpFileSync if sync file cannot be created or if already joined.
  544. }
  545. function TGpFileGroup.Join(timeout: DWORD; var isFirstMember: boolean): boolean;
  546. var
  547.   err: DWORD;
  548. begin
  549.   if IsMember then
  550.     raise EGpFileSync.CreateFmt(SAlreadyJoined,[SyncFile])
  551.   else begin
  552.     if not fgLock.Acquire(timeout) then
  553.       Result := false
  554.     else begin
  555.       try
  556.         fgHandle := CreateFile(PChar(SyncFile),GENERIC_READ,0,nil,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
  557.         if fgHandle <> INVALID_HANDLE_VALUE then begin
  558.           isFirstMember := true;
  559.           CloseHandle(fgHandle);
  560.           fgHandle := INVALID_HANDLE_VALUE;
  561.         end
  562.         else begin
  563.           err := GetLastError;
  564.           if err in FILE_SHARING_ERRORS then
  565.             isFirstMember := false
  566.           else
  567.             raise EGpFileSync.CreateFmt(SCannotAccessFile,[SyncFile,SysErrorMessage(err)]);
  568.         end;
  569.         fgHandle := CreateFile(PChar(SyncFile),GENERIC_READ,FILE_SHARE_READ,nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
  570.         if fgHandle = INVALID_HANDLE_VALUE then
  571.           raise EGpFileSync.CreateFmt(SCannotAccessFile,[SyncFile,SysErrorMessage(GetLastError)]);
  572.       finally fgLock.Release; end;
  573.       Result := true;
  574.     end;
  575.   end;
  576. end; { TGpFileGroup.Join }
  577.  
  578. {:Leaves the group.
  579.   @param   timeout       Timeout in milliseconds. 0 and INFINITE are supported.
  580.   @param   wasLastMember (out) Set to true if this was last process in the
  581.                          group. Defined only if function returns true.
  582.   @returns false on timeout.
  583.   @raises  EGpFileSync if sync file cannot be deleted and this was last member
  584.            and deleteOnRelease was required. Also raised if not joined.
  585. }
  586. function TGpFileGroup.Leave(timeout: DWORD; var wasLastMember: boolean): boolean;
  587. var
  588.   err: DWORD;
  589. begin
  590.   if not IsMember then
  591.     raise EGpFileSync.CreateFmt(SNotJoined,[SyncFile])
  592.   else begin
  593.     if not fgLock.Acquire(timeout) then
  594.       Result := false
  595.     else begin
  596.       try
  597.         CloseHandle(fgHandle);
  598.         fgHandle := CreateFile(PChar(SyncFile),GENERIC_READ,0,nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
  599.         if fgHandle <> INVALID_HANDLE_VALUE then begin
  600.           wasLastMember := true;
  601.           CloseHandle(fgHandle);
  602.           fgHandle := INVALID_HANDLE_VALUE;
  603.           if fgDelete then
  604.             if not Windows.DeleteFile(PChar(SyncFile)) then
  605.               raise EGpFileSync.CreateFmt(SCannotDeleteFile,[SyncFile,SysErrorMessage(GetLastError)]);
  606.         end
  607.         else begin
  608.           err := GetLastError;
  609.           if err in FILE_SHARING_ERRORS then
  610.             wasLastMember := false
  611.           else
  612.             raise EGpFileSync.CreateFmt(SCannotAccessFile,[SyncFile,SysErrorMessage(err)]);
  613.         end;
  614.       finally fgLock.Release; end;
  615.       Result := true;
  616.     end;
  617.   end;
  618. end; { TGpFileGroup.Leave }
  619.  
  620. {:Overloaded Join, does not return status.
  621.   @param   timeout Timeout in milliseconds. 0 and INFINITE are supported.
  622.   @returns false on timeout.
  623.   @raises  EGpFileSync if sync file cannot be created or if already joined.
  624. }
  625. function TGpFileGroup.Join(timeout: DWORD): boolean;
  626. var
  627.   isFirst: boolean;
  628. begin
  629.   Result := Join(timeout,isFirst);
  630. end; { TGpFileGroup.Join }
  631.  
  632. {:Overloaded Leave, does not return status.
  633.   @param   timeout Timeout in milliseconds. 0 and INFINITE are supported.
  634.   @returns false on timeout.
  635.   @raises  EGpFileSync if sync file cannot be deleted and this was last member
  636.            and deleteOnRelease was required. Also raised if not joined.
  637. }
  638. function TGpFileGroup.Leave(timeout: DWORD): boolean;
  639. var
  640.   wasLast: boolean;
  641. begin
  642.   Result := Leave(timeout,wasLast);
  643. end; { TGpFileGroup.Leave }
  644.  
  645. {:Checks if file group is empty.
  646.   @param   timeout    Timeout in milliseconds. 0 and INFINITE are supported.
  647.   @param   emptyGroup (out) Set to true if group is empty. Defined only if
  648.                       function returns true.
  649.   @returns false on timeout.
  650. }
  651. function TGpFileGroup.IsEmpty(timeout: DWORD; var emptyGroup: boolean): boolean;
  652. var
  653.   err: DWORD;
  654. begin
  655.   Result := false;
  656.   if not IsMember then begin
  657.     if fgLock.Acquire(timeout) then begin
  658.       try
  659.         fgHandle := CreateFile(PChar(SyncFile),GENERIC_READ,0,nil,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
  660.         if fgHandle <> INVALID_HANDLE_VALUE then begin
  661.           CloseHandle(fgHandle);
  662.           fgHandle := INVALID_HANDLE_VALUE;
  663.           emptyGroup := true;
  664.         end
  665.         else begin
  666.           err := GetLastError;
  667.           if not (err in FILE_SHARING_ERRORS) then
  668.             raise EGpFileSync.CreateFmt(SCannotAccessFile,[SyncFile,SysErrorMessage(err)]);
  669.           emptyGroup := false;
  670.         end;
  671.       finally fgLock.Release; end;
  672.       Result := true;
  673.     end;
  674.   end;
  675. end; { TGpFileGroup.IsEmpty }
  676.  
  677. {:Checks if instance is already member of group.
  678.   @returns True if instance is already member of group.
  679.   @since   2000-11-16
  680. }
  681. function TGpFileGroup.IsMember: boolean;
  682. begin
  683.   Result := (fgHandle <> INVALID_HANDLE_VALUE);
  684. end; { TGpFileGroup.IsMember }
  685.  
  686. procedure TGpFileGroup.SetRetryDelay(const Value: integer);
  687. begin
  688.   inherited;
  689.   if assigned(fgLock) then
  690.     fgLock.RetryDelay := Value;
  691. end; { TGpFileGroup.SetRetryDelay }
  692.  
  693. { TGpFileMessage }
  694.  
  695. {:TGpFileMessage constructor.
  696.   @raises EGpFileSync if program doesn't have rights to create syncFile.
  697. }
  698. constructor TGpFileMessage.Create(syncFile: string);
  699. begin
  700.   inherited Create(syncFile,true);
  701.   fmLock := TGpFileMutex.Create(syncFile+'_lck',true);
  702.   fmGroup := TGpFileGroup.Create(syncFile+'_grp',true);
  703.   fmLock.RetryDelay := RetryDelay;
  704.   fmGroup.RetryDelay := RetryDelay;
  705. end; { TGpFileMessage.Create }
  706.  
  707. {:TGpFileMessage destructor. 
  708. }
  709. destructor TGpFileMessage.Destroy;
  710. begin
  711.   FreeAndNil(fmGroup);
  712.   FreeAndNil(fmLock);
  713.   inherited;
  714. end; { TGpFileMessage.Destroy }
  715.  
  716. {:Receives message. 
  717.   @param   timeout   Timeout. INFINITE and 0 are supported.
  718.   @param   msg       Pointer to message data. Will be allocated in Receive with a
  719.                      call to GetMem and should be freed in caller program with a
  720.                      call to FreeMem. Set only when function returns true.
  721.   @param   msgSize   Size of message data. Set only when function returns true.
  722.   @returns true      If message was received in time.
  723.   @raises  EGpFileSync on various errors.
  724. }
  725. function TGpFileMessage.Receive(timeout: DWORD; var msg: pointer; var msgSize: integer): boolean;
  726. var
  727.   err    : DWORD;
  728.   h      : THandle;
  729.   isFirst: boolean;
  730.   read   : DWORD;
  731.   start  : int64;
  732. begin
  733.   Result := false;
  734.   start := GetTickCount;
  735.   repeat
  736.     if not fmLock.Acquire(timeout) then
  737.       raise EGpFileSync.CreateFmt(SCannotAcquireMutex,[SyncFile])
  738.     else begin
  739.       try
  740.         if not fmGroup.Join(timeout,isFirst) then
  741.           raise EGpFileSync.CreateFmt(SCannotJoinGroup,[SyncFile])
  742.         else begin
  743.           try
  744.             if not isFirst then begin
  745.               h := CreateFile(PChar(SyncFile),GENERIC_READ,0,nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
  746.               if h = INVALID_HANDLE_VALUE then begin
  747.                 err := GetLastError;
  748.                 if err <> ERROR_FILE_NOT_FOUND then
  749.                   raise EGpFileSync.CreateFmt(SCannotAccessFile,[SyncFile,SysErrorMessage(err)]);
  750.               end
  751.               else begin
  752.                 try
  753.                   msgSize := GetFileSize(h,nil);
  754.                   GetMem(msg,msgSize); // will be Free'd by the caller
  755.                   if not ReadFile(h,msg^,msgSize,read,nil) then begin
  756.                     FreeMem(msg);
  757.                     raise EGpFileSync.CreateFmt(SCannotReadFromFile,[SyncFile,SysErrorMessage(GetLastError)])
  758.                   end
  759.                   else if cardinal(msgSize) <> read then begin
  760.                     FreeMem(msg);
  761.                     raise EGpFileSync.CreateFmt(SIncorrectNumRead,[SyncFile]); 
  762.                   end;
  763.                 finally CloseHandle(h); end;
  764.                 // no exception - all OK - continue
  765.                 if not Windows.DeleteFile(PChar(SyncFile)) then
  766.                   raise EGpFileSync.CreateFmt(SCannotDeleteFile,[SyncFile,SysErrorMessage(GetLastError)])
  767.                 else begin
  768.                   Result := true;
  769.                   Exit;
  770.                 end;
  771.               end;
  772.             end;
  773.           finally fmGroup.Leave(timeout); end;
  774.         end;
  775.       finally fmLock.Release; end;
  776.     end;
  777.     if Elapsed(start,timeout) then
  778.       break;
  779.     Sleep(RetryDelay);
  780.   until false;
  781. end; { TGpFileMessage.Receive }
  782.  
  783. {:Sends a message and waits on recepient to read it. If there is already a
  784.   message waiting to be received, it will be overwritten. If process crashes
  785.   while sending, message will be left on disk but receiving process will know
  786.   that it is invalid.
  787.   @param   timeout   Timeout. INFINITE is supported. 0 is supported but useless.
  788.   @param   msg       Message data.
  789.   @param   msgSize   Message size.
  790.   @returns false     if nobody picks up message in specified time.
  791.   @raises  EGpFileSync on various errors.
  792. }
  793. function TGpFileMessage.Send(timeout: DWORD; msg: pointer; msgSize: integer): boolean;
  794. var
  795.   h      : THandle;
  796.   isFirst: boolean;
  797.   start  : int64;
  798.   written: DWORD;
  799. begin
  800.   Result := false;
  801.   if not fmLock.Acquire(timeout) then
  802.     raise EGpFileSync.CreateFmt(SCannotAcquireMutex,[SyncFile])
  803.   else begin
  804.     try
  805.       if not fmGroup.Join(timeout,isFirst) then
  806.         raise EGpFileSync.CreateFmt(SCannotJoinGroup,[SyncFile])
  807.       else begin
  808.         try
  809.           if not isFirst then
  810.             raise EGpFileSync.CreateFmt(SGroupNotEmpty,[SyncFile])
  811.           else begin
  812.             h := CreateFile(PChar(SyncFile),GENERIC_READ+GENERIC_WRITE,0,nil,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
  813.             if h = INVALID_HANDLE_VALUE then
  814.               raise EGpFileSync.CreateFmt(SCannotCreateFile,[SyncFile,SysErrorMessage(GetLastError)])
  815.             else begin
  816.               try
  817.                 if not WriteFile(h,msg^,msgSize,written,nil) then
  818.                   raise EGpFileSync.CreateFmt(SCannotWriteToFile,[SyncFile,SysErrorMessage(GetLastError)])
  819.                 else if written <> DWORD(msgSize) then
  820.                   raise EGpFileSync.CreateFmt(SIncorrectNumWritten,[SyncFile])
  821.               finally CloseHandle(h); end;
  822.             end;
  823.             // no exception - all OK - continue
  824.             start := GetTickCount;
  825.             repeat
  826.               if not FileExists(SyncFile) then begin
  827.                 Result := true;
  828.                 break;
  829.               end
  830.               else if Elapsed(start,timeout) then begin
  831.                 if not Windows.DeleteFile(PChar(SyncFile)) then
  832.                   raise EGpFileSync.CreateFmt(SCannotDeleteFile,[SyncFile,SysErrorMessage(GetLastError)])
  833.                 else
  834.                   break;
  835.               end
  836.               else begin
  837.                 fmLock.Release;
  838.                 Sleep(RetryDelay);
  839.                 if not fmLock.Acquire(timeout) then
  840.                   raise EGpFileSync.CreateFmt(SCannotAcquireMutex,[SyncFile]);
  841.               end;
  842.             until false;
  843.           end;
  844.         finally fmGroup.Leave(timeout); end;
  845.       end;
  846.     finally
  847.       if fmLock.Acquired then
  848.         fmLock.Release;
  849.     end;
  850.   end;
  851. end; { TGpFileMessage.Send }
  852.  
  853. procedure TGpFileMessage.SetRetryDelay(const Value: integer);
  854. begin
  855.   inherited;
  856.   if assigned(fmLock) then
  857.     fmLock.RetryDelay := Value;
  858.   if assigned(fmGroup) then
  859.     fmGroup.RetryDelay := Value;
  860. end; { TGpFileMessage.SetRetryDelay }
  861.  
  862. { TGpFileSWMR }
  863.  
  864. {:TGpFileSWMR constructor.
  865.   @param syncFileBase    Synchronisation file base name.
  866.   @param deleteOnRelease If set, synchronisation files will be deleted when not
  867.                          used. This requires write access to synchronisation
  868.                          folder.
  869.   @raises EGpFileSync if synchronisation files do not exist and write access to
  870.           synchronisation folder is not allowed.
  871. }
  872. constructor TGpFileSWMR.Create(syncFileBase: string; deleteOnRelease: boolean);
  873. begin
  874.   inherited Create;
  875.   fswmrSyncFileBase := syncFileBase;
  876.   fswmrMutex1 := TGpFileMutex.Create(syncFileBase+'_lck1',deleteOnRelease);
  877.   fswmrMutex2 := TGpFileMutex.Create(syncFileBase+'_lck2',deleteOnRelease);
  878.   fswmrGroup  := TGpFileGroup.Create(syncFileBase+'_grp',deleteOnRelease);
  879.   fswmrMutex1.RetryDelay := RetryDelay;
  880.   fswmrMutex2.RetryDelay := RetryDelay;
  881.   fswmrGroup.RetryDelay := RetryDelay;
  882. end; { TGpFileSWMR.Create }
  883.  
  884. destructor TGpFileSWMR.Destroy;
  885. begin
  886.   if IsReading then
  887.     DoneReading(CAutoDestroyTimeout);
  888.   if IsWriting then
  889.     DoneWriting;
  890.   FreeAndNil(fswmrMutex1);
  891.   FreeAndNil(fswmrMutex2);
  892.   FreeAndNil(fswmrGroup);
  893.   inherited;
  894. end; { TGpFileSWMR.Destroy }
  895.  
  896. {:Releases read lock.
  897.   @param   timeout Timeout. 0 and INFINITE are supported.
  898.   @raises  EGpFileSync if doesn't have read access to SWMR.
  899. }
  900. function TGpFileSWMR.DoneReading(timeout: DWORD): boolean;
  901. begin
  902.   if not IsReading then
  903.     raise EGpFileSync.CreateFmt(SNotReading,[SyncFile])
  904.   else
  905.     Result := fswmrGroup.Leave(timeout);
  906. end; { TGpFileSWMR.DoneReading }
  907.  
  908. {:Releases write lock.
  909.   @raises  EGpFileSync if doesn't have write access to SWMR.
  910. }
  911. procedure TGpFileSWMR.DoneWriting;
  912. begin
  913.   if not IsWriting then
  914.     raise EGpFileSync.CreateFmt(SNotWriting,[SyncFile])
  915.   else
  916.     fswmrMutex2.Release;
  917. end; { TGpFileSWMR.DoneWriting }
  918.  
  919. {:Checks if SWMR is acquired for reading.
  920.   @returns True if SWMR is acquired for reading.
  921. }
  922. function TGpFileSWMR.IsReading: boolean;
  923. begin
  924.   Result := fswmrGroup.IsMember;
  925. end; { TGpFileSWMR.IsReading }
  926.  
  927. {:Checks if SWMR is acquired for writing
  928.   @returns True if SWMR is acquired for writing.
  929. }
  930. function TGpFileSWMR.IsWriting: boolean;
  931. begin
  932.   Result := fswmrMutex2.Acquired;
  933. end; { TGpFileSWMR.IsWriting }
  934.  
  935. {:Sets RetryDelay for SWMR and subcomponents.
  936. }
  937. procedure TGpFileSWMR.SetRetryDelay(const Value: integer);
  938. begin
  939.   fswmrRetryDelay := Value;
  940.   if assigned(fswmrMutex1) then
  941.     fswmrMutex1.RetryDelay := Value;
  942.   if assigned(fswmrMutex2) then
  943.     fswmrMutex2.RetryDelay := Value;
  944.   if assigned(fswmrGroup) then
  945.     fswmrGroup.RetryDelay := Value;
  946. end; { TGpFileSWMR.SetRetryDelay }
  947.  
  948. {:Waits until timeout occurs or until process is allowed read access to SWMR.
  949.   @param   timeout Timeout. 0 and INFINITE are supported.
  950.   @returns True if read access was allowed.
  951.   @raises  EGpFileSync if if synchronisation files do not exist and write access to
  952.            synchronisation folder is not allowed.
  953.            EGpFileSync if already has read access.
  954. }
  955. function TGpFileSWMR.WaitToRead(timeout: DWORD): boolean;
  956. var
  957.   start: int64;
  958. begin
  959.   if IsReading then
  960.     raise EGpFileSync.CreateFmt(SAlreadyReading,[SyncFile]);
  961.   start := GetTickCount;
  962.   repeat
  963.     if fswmrMutex1.Acquire(timeout) then begin
  964.       try
  965.         if fswmrMutex2.IsFree(timeout) then
  966.           fswmrGroup.Join(timeout);
  967.       finally fswmrMutex1.Release; end;
  968.     end;
  969.     if fswmrGroup.IsMember or Elapsed(start,timeout) then
  970.       break;
  971.     Sleep(RetryDelay);
  972.   until false;
  973.   Result := IsReading;
  974. end; { TGpFileSWMR.WaitToRead }
  975.  
  976. {:Waits until timeout occurs or until process is allowed write access to SWMR.
  977.   @param   timeout Timeout. 0 and INFINITE are supported.
  978.   @returns True if write access was allowed.
  979.   @raises  EGpFileSync if if synchronisation files do not exist and write access to
  980.            synchronisation folder is not allowed.
  981.            EGpFileSync if already has write access.
  982. }
  983. function TGpFileSWMR.WaitToWrite(timeout: DWORD): boolean;
  984. var
  985.   isEmptyGroup: boolean;
  986.   start       : int64;
  987. begin
  988.   if IsWriting then
  989.     raise EGpFileSync.CreateFmt(SAlreadyWriting,[SyncFile]);
  990.   start := GetTickCount;
  991.   repeat
  992.     if fswmrMutex1.Acquire(timeout) then begin
  993.       try
  994.         if not fswmrGroup.IsEmpty(timeout,isEmptyGroup) then
  995.           isEmptyGroup := false;
  996.         if isEmptyGroup then // no readers
  997.           if not fswmrMutex2.Acquire(timeout) then
  998.             raise EGpFileSync.CreateFmt(SCannotAcquireMutex,[fswmrMutex2.SyncFile]);
  999.       finally fswmrMutex1.Release; end;
  1000.     end;
  1001.     if fswmrMutex2.Acquired or Elapsed(start,timeout) then
  1002.       break;
  1003.     Sleep(RetryDelay);
  1004.   until false;
  1005.   Result := IsWriting;
  1006. end; { TGpFileSWMR.WaitToWrite }
  1007.  
  1008. end.
  1009.